Skip to content

Restore priming - #713

Draft
zhengyu123 wants to merge 40 commits into
mainfrom
zgu/thread_priming
Draft

Restore priming#713
zhengyu123 wants to merge 40 commits into
mainfrom
zgu/thread_priming

Conversation

@zhengyu123

@zhengyu123 zhengyu123 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?:
Problem
ProfiledThread::current() returns nullptr for any thread the profiler hasn't explicitly registered yet. That's fine for normal application threads (they go through JVMTI's ThreadStart callback before doing anything interesting), but several JVM-internal threads — most notably JIT compiler threads (C1/C2 CompilerThread on HotSpot, JIT Compilation Thread on OpenJ9) — start very early during JVM bootstrap, often before the profiler agent has attached at all. Those threads never go through the normal registration path, so every profiling signal that lands on them is silently dropped: no ProfiledThread, no sample, no visibility into compiler-thread CPU cost.

Solution
Add TLS priming: the ability to attach a ProfiledThread to a thread on demand, from inside the signal handler itself, the first time that thread is sampled.

ThreadLocalDataPool(new threadLocalDataPool.h/.cpp): a fixed-capacity (64), pre-allocated pool of ProfiledThread slots. Slots are claimed/released via atomic CAS on a FLAG_CLAIMED bit — no locks, no allocation, safe to call from a signal handler.
ProfiledThread::acquireCurrent() (new threadLocalData.inline.h): get-or-prime. Returns the existing TLS value if set; otherwise claims a pool slot and attaches it via pthread_setspecific, right there in the signal handler.
ProfiledThread::supportPriming(): gates whether priming is safe at all. On glibc, pthread_setspecific can malloc internally unless the thread's pthread_key_t falls in the NPTL's pre-allocated first-level array (< PTHREAD_KEY_2NDLEVEL_SIZE) — priming is only enabled when that's guaranteed. Always enabled on musl. Fails safe (disabled) on any other libc (e.g. macOS), since the glibc-specific check doesn't apply there.
stackWalker.cpp / hotspotSupport.cpp (walkFP, walkDwarf, walkVM, walkJavaStack) switched from current() to acquireCurrent(), with an early return + SAMPLES_DROPPED_THREAD_LOCAL counter bump when priming isn't possible (pool exhausted, or unsupported on this libc) — replacing the old != nullptr ternary chains.

Supporting changes
threadLocalData.h's current()/acquireCurrent() moved to a new threadLocalData.inline.h (needed to avoid a circular include with ThreadLocalDataPool); ~15 .cpp files updated to include it.
New INJECT_FAULT_BOOL_HIGH fault-injection tier (10% firing rate) used to exercise the supportPriming() false-path in fault-injection builds.
UnwindFailures gained a reset() so a recycled pool slot can be reused without a fresh malloc.
New SAMPLES_DROPPED_TLS_POOL_EXHAUSTED counter for observability when the pool runs out of slots.

Motivation:

Additional Notes:

How to test the change?:

threadLocalDataPool_ut.cpp (new): boundary tests for contains() (first/last/one-past-end/one-before/null slot), plus a targeted regression test for the used >= _capacity vs. used > _capacity off-by-one in claim() — it checks the SAMPLES_DROPPED_TLS_POOL_EXHAUSTED counter rather than the return value, since both variants return nullptr at capacity but only the buggy one falls through to the exhaustion-counting scan.
TlsPrimingTest.java (new): end-to-end validation — forces sustained JIT compilation via a dynamically-generated class, then asserts datadog.ExecutionSample events include samples whose eventThread is a compiler thread. This is the test that actually proves priming works: if it silently broke, compiler threads would just never show up as eventThread and this test would fail.

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles
    credentials of any kind, I've requested a security review (run the dd:platform-security-review
    skill, or file a request via the PSEC review form).
    bewaire also runs automatically on every PR.
  • This PR doesn't touch any of that.
  • JIRA: PROF-15601

Unsure? Have a question? Request a review!

Copilot AI review requested due to automatic review settings August 3, 2026 19:56
@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmvrwv9
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Fri Aug 7 20:53:40 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerprofiler.hfindLibraryByAddress51714

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reintroduces “priming” for ProfiledThread access in async/signal-handling stack-walk paths by adding a preallocated ThreadLocalDataPool and switching key sampling sites to acquire and cache a ProfiledThread without per-signal allocation.

Changes:

  • Added ThreadLocalDataPool plus ProfiledThread::acquire_current() to acquire/carry a reusable ProfiledThread in signal context.
  • Updated stack walking and sampling code paths (StackWalker/HotSpot) to use acquire_current() and track drops when TLS cannot be acquired.
  • Rewired a number of translation units to include threadLocalData.inline.h (moving the inline TLS accessors out of threadLocalData.h).

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ddprof-lib/src/main/cpp/wallClock.h Switch to threadLocalData.inline.h include for inlined TLS access.
ddprof-lib/src/main/cpp/threadLocalDataPool.h New pool API for reusing ProfiledThread instances.
ddprof-lib/src/main/cpp/threadLocalDataPool.cpp New pool implementation: allocate/claim/unclaim pooled ProfiledThread slots.
ddprof-lib/src/main/cpp/threadLocalData.inline.h New header providing inline definitions of ProfiledThread::current() and acquire_current().
ddprof-lib/src/main/cpp/threadLocalData.h Adds claimed flag/state and declares new inline TLS accessors.
ddprof-lib/src/main/cpp/threadLocalData.cpp Routes TLS destructor cleanup through the pool when applicable.
ddprof-lib/src/main/cpp/stackWalker.cpp Uses acquire_current() and increments drop counter when TLS cannot be acquired.
ddprof-lib/src/main/cpp/refCountGuard.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/perfEvents_linux.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/jvmThread.h Adds supportPriming() decision helper (musl vs glibc TLS key range).
ddprof-lib/src/main/cpp/jvmSupport.cpp Initializes ThreadLocalDataPool when priming is supported.
ddprof-lib/src/main/cpp/javaApi.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/itimer.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp Uses acquire_current() in HotSpot stack-walk paths.
ddprof-lib/src/main/cpp/guards.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/flightRecorder.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/ctimer_linux.cpp Switch include to threadLocalData.inline.h.
ddprof-lib/src/main/cpp/context_api.cpp Switch include to threadLocalData.inline.h.
Suppressed comments (1)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:64

  • initialize() publishes the pool pointer unconditionally; if construction failed (e.g., _threads == nullptr), subsequent acquire()/release() calls can hit UB. Only publish the pool if it is usable; otherwise keep _pool null.
void ThreadLocalDataPool::initialize() {
    ThreadLocalDataPool* pool = new ThreadLocalDataPool();
    __atomic_store_n(&_pool, pool, __ATOMIC_RELEASE);
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.h Outdated
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
Comment thread ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 20:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Suppressed comments (7)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:18

  • ThreadLocalDataPool doesn’t initialize _threads when malloc fails, leaving it indeterminate. That can lead to invalid free() in the destructor and crashes in claim()/contains(). Initialize _threads to nullptr in the ctor initializer list.
ThreadLocalDataPool::ThreadLocalDataPool(uint64_t capacity) : _capacity(capacity), _used(0) {
    size_t malloc_size = capacity * sizeof(ProfiledThread);
    void* p = malloc(malloc_size);
    if (p != nullptr) {
      _threads = reinterpret_cast<ProfiledThread*>(p);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:48

  • On probe failure, claim() currently assert(false) after scanning the whole pool. Under races (or if _threads is unexpectedly null), this can abort the process from a signal handler. Prefer to roll back _used and return nullptr (dropping the sample) instead of asserting.
    do {
        if (_threads[index].claim_acquire(tid)) {
            return &_threads[index];
        }
        index = (index + 1) % _capacity;
    } while (index != start_pos);
    assert(false && "Should not reach here");
    return nullptr;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:56

  • unclaim() has the same unsigned-decrement issue as claim() (using __atomic_fetch_add(..., -1, ...) on a uint16_t). Use __atomic_fetch_sub(..., 1, ...) so _used doesn’t wrap.
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);
        return true;

ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp:1226

  • The previous code restored ThreadLocalData::_unwinding_Java after a recovered siglongjmp because siglongjmp bypasses AsyncSampleMutex destructors. That restore was removed, so a crash recovery can leave _unwinding_Java stuck true, preventing future Java stack walks on the thread.
  if (sigsetjmp(crash_protection_ctx, 1) != 0) {
    // checkFault() does a siglongjmp from inside segvHandler, bypassing
    // segvHandler's SignalHandlerScope destructor. Compensate.
    SIGNAL_HANDLER_UNWIND_AFTER_LONGJMP();
    prof_thread->setJmpCtx(prev_jmp_buf);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:55

  • unclaim() reconstructs ProfiledThread via placement-new while other threads may concurrently probe/claim the same slot. Because ProfiledThread uses atomic operations on _misc_flags, reinitializing it with non-atomic stores (constructor/placement-new) can race with those atomics (UB) and also makes it possible to observe a partially-reset object. Consider adding an explicit “reset for pool reuse” routine that keeps the slot in a claimed state while resetting fields, then clears FLAG_CLAIMED with a release store as the final step.
bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);

ddprof-lib/src/main/cpp/threadLocalData.inline.h:18

  • ProfiledThread::current() is defined here, but if it’s also defined inline in threadLocalData.h (to keep existing include sites compiling), this becomes a duplicate definition across TUs. Keep only one definition (e.g., define current() in threadLocalData.h and leave only acquire_current() here).
ProfiledThread* ProfiledThread::current() {
    if (!isThreadKeyValid()) {
      return nullptr;
    }
    return _current_thread.get();
}

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:37

  • __atomic_fetch_add(&_used, -1, ...) is performed on a uint16_t. The -1 is converted to uint16_t (65535), so this increments by 65535 (wraps) rather than decrementing. Use __atomic_fetch_sub(..., 1, ...) (and similarly in unclaim).

This issue also appears on line 53 of the same file.

    uint16_t used = __atomic_fetch_add(&_used, 1, __ATOMIC_RELAXED);
    if (used >= _capacity) {
        __atomic_fetch_add(&_used, -1, __ATOMIC_RELAXED);
        return nullptr;

Comment thread ddprof-lib/src/main/cpp/threadLocalData.h
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #31217653203 | Commit: 537770b | Duration: 14m 11s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-08-07 21:09:15 UTC

@dd-octo-sts

dd-octo-sts Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 856ee13)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128699821 Commit: 856ee133a68fe770ffcfa01dd89dffccb4305ee2

⚠️ Significant outliers

  • 🟢 fj-kmeans (JDK 21): runtime -4.5% (2778→2653 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10326 ms (21 iters) ✅ 10362 ms (21 iters) ≈ +0.3% (±11.4%) — / —
finagle-chirper 21 ✅ 5952 ms (33 iters) ✅ 5955 ms (33 iters) ≈ +0.1% (±25.5%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5484 ms (36 iters) ✅ 5471 ms (36 iters) ≈ -0.2% (±24.1%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2778 ms (67 iters) ✅ 2653 ms (71 iters) 🟢 -4.5% — / —
fj-kmeans 25 ✅ 2764 ms (68 iters) ✅ 2759 ms (68 iters) ≈ -0.2% (±2.8%) — / —
future-genetic 21 ✅ 2060 ms (90 iters) ✅ 2114 ms (87 iters) ≈ +2.6% (±2.7%) — / —
future-genetic 25 ✅ 2053 ms (90 iters) ✅ 2009 ms (93 iters) ≈ -2.1% (±2.5%) — / —
naive-bayes 21 ✅ 1268 ms (135 iters) ✅ 1298 ms (132 iters) ≈ +2.4% (±33.2%) — / —
reactors 21 ✅ 16232 ms (15 iters) ✅ 16628 ms (16 iters) ≈ +2.4% (±8.8%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 5 / 3 2054 / 1956 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 2 / 2 8800 / 8383 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 1 / 1 8585 / 8202 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1279 / 1277 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 1 / 4 2914 / 2956 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 1 / ✅ 2797 / 2885 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 7 / 2 3545 / 3557 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ 1 / ✅ 1581 / 1852 ✅ / ✅ ✅ / ✅

Copilot AI review requested due to automatic review settings August 3, 2026 20:57
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@datadog-datadog-us1-prod

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:58

  • unclaim() reconstructs the slot with placement-new (new (t) ProfiledThread(0)), which writes _misc_flags (and other fields) non-atomically while other threads may concurrently read _misc_flags via __atomic_* in claim_acquire(). This mixes atomic and non-atomic accesses to the same object and can also momentarily clear FLAG_CLAIMED before the slot reset is fully complete.
    return nullptr;
}

bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:15

  • If malloc() fails in ThreadLocalDataPool's constructor, _threads is left uninitialized, but later code (including the destructor and contains()) assumes it is either a valid pointer or nullptr. This can lead to undefined behavior.
ThreadLocalDataPool::ThreadLocalDataPool(uint64_t capacity)

ddprof-lib/src/main/cpp/threadLocalData.h:125

  • The assertion message in ProfiledThread::unclaim() is inverted: if the assert fires, the slot was not claimed, but the message says it "has been claimed".
    assert(isClaimed() && "Slot has been claimed");

Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp
Comment thread ddprof-lib/src/main/cpp/jvmThread.h Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 21:03
@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bits found no code fix to apply

🟢 Investigated · ⚪ No code fix needed

test-matrix / test-linux-glibc-amd64 (11-j9, debug) fails because Gradle Test Executor 1 exits with status 1 after reported tests complete, but the available logs contain no failed assertion, JVM crash report, or test-process diagnostic. The matching job passed on the base commit, but the missing root-cause evidence makes a code change unsafe; the failure artifact or full executor stderr is needed to identify the responsible path.


View in Datadog | Reviewed commit bd5bd61 · Any feedback? Reach out in #deveng-pr-agent

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:43

  • ThreadLocalDataPool::claim() currently has a broken/missing capacity guard: the code unconditionally decrements _used and returns nullptr, and the braces are unbalanced, so this won’t compile and the pool can never hand out slots. Add the intended used >= _capacity check and close the block correctly.
    uint16_t used = __atomic_fetch_add(&_used, 1, __ATOMIC_RELAXED);
        __atomic_fetch_add(&_used, -1, __ATOMIC_RELAXED);
        return nullptr;
    }

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:60

  • unclaim() reconstructs the slot with placement-new (ProfiledThread(0)), which clears FLAG_CLAIMED via a non-atomic write to _misc_flags. That allows another thread to observe the slot as unclaimed and race in while the object is mid-reset. Prefer clearing the claimed bit atomically (the class already provides ProfiledThread::unclaim() for this) and let the next acquire() reinitialize the object.
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);

ddprof-lib/src/main/cpp/threadLocalData.inline.h:20

  • ProfiledThread::acquire_current() is defined in a header included by multiple translation units but is not marked inline, which can produce multiple-definition linker errors. Mark it inline.
ProfiledThread* ProfiledThread::acquire_current() {

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:12

  • This file uses placement-new (new (&_threads[index]) ...) but does not include <new>, which is required to declare placement-new in standard C++. Add the missing include to avoid build failures on stricter toolchains.
#include <cassert>
#include <stdlib.h>

Comment thread ddprof-lib/src/main/cpp/threadLocalData.inline.h Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 3, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Suppressed comments (8)

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:62

  • _used was widened in the header, but this local variable is still uint16_t, which will truncate the atomic counter and can underflow/wrap incorrectly.
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:63

  • unclaim() reconstructs the ProfiledThread in-place with placement-new while other threads may concurrently read/modify the slot (via claim_acquire() / _misc_flags). Re-ending/restarting an object’s lifetime and doing non-atomic writes to the same storage other threads touch is undefined behavior and can lead to double-claim or corrupted state. Consider keeping slot ownership state separate from the ProfiledThread object (e.g., a dedicated std::atomic<uint32_t> claim word per slot) and avoid placement-new on shared objects; reset per-thread state only after exclusive ownership is established.
bool ThreadLocalDataPool::unclaim(ProfiledThread* t) {
    if (contains(t)) {
        new (t)ProfiledThread(0);
        uint16_t used = __atomic_fetch_add(&_used, -1, __ATOMIC_RELEASE);
        assert(used > 0);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:83

  • acquire() reconstructs a claimed ProfiledThread in-place. Even though the slot is "claimed", other threads may still probe its claim state concurrently (via _misc_flags), and reconstructing the object restarts its lifetime while concurrent reads are possible. This is undefined behavior in C++ and can manifest as intermittent races. Prefer a separate per-slot claim flag (outside the object being reconstructed), or avoid placement-new and instead reset fields under exclusive ownership without touching memory concurrently accessed by other threads.
        ProfiledThread* t = pool->claim(tid);
        if (t != nullptr) {
            new (t)ProfiledThread(tid, true /* claimed */);
        }
        return t;

ddprof-lib/src/main/cpp/threadLocalData.h:183

  • ProfiledThread::current() is declared inline here but no longer defined in this header. Several existing translation units still include threadLocalData.h (not threadLocalData.inline.h) and call ProfiledThread::current(), which will fail to compile. Either keep current() defined here (as before) or make threadLocalData.h include the inline definitions.
  // Signal-handler friendly (no allocation): returns existing TLS or nullptr.
  static inline ProfiledThread *current();
  // signal-handler friendly with priming: return existing TLS or acquire and set
  // ProfiledThread from ThreadLocalDataPool.
  static inline ProfiledThread* acquire_current();

ddprof-lib/src/main/cpp/threadLocalData.inline.h:13

  • With ProfiledThread::current() defined back in threadLocalData.h, this out-of-class definition becomes a duplicate definition when threadLocalData.inline.h is included (directly or indirectly). Remove it to avoid redefinition errors.
inline ProfiledThread* ProfiledThread::current() {

ddprof-lib/src/main/cpp/threadLocalDataPool.h:20

  • _used is a 16-bit counter but _capacity is 64-bit; if the pool capacity is ever increased beyond 65535, _used will wrap and the full/empty checks become incorrect. Use a wider counter type that can represent _capacity.
    const uint64_t      _capacity;
    volatile uint16_t   _used;
    ProfiledThread*     _threads;

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:41

  • _used was widened in the header, but this local variable is still uint16_t, which will truncate the atomic counter and break capacity checks once _used exceeds 65535.

This issue also appears on line 62 of the same file.

    uint16_t used = __atomic_fetch_add(&_used, 1, __ATOMIC_RELAXED);

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:72

  • This PR introduces a new concurrent, signal-path critical allocation strategy (ThreadLocalDataPool + ProfiledThread::acquire_current()), but there are no accompanying C++ unit tests validating pool exhaustion behavior, claim/release correctness, or the interaction with TLS teardown (ProfiledThread::freeValue). The repo has an existing gtest suite under ddprof-lib/src/test/cpp/; please add targeted tests to lock in correctness.
void ThreadLocalDataPool::initialize() {
    ThreadLocalDataPool* pool = new ThreadLocalDataPool();
    __atomic_store_n(&_pool, pool, __ATOMIC_RELEASE);
}

Comment thread ddprof-lib/src/main/cpp/threadLocalDataPool.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/jvmSupport.cpp Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13c6c30d05

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ddprof-lib/src/main/cpp/guards.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/mallocTracer.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/unwindStats.cpp

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:86

  • The HotSpot branch only checks a single prefix via startsWith(), so it won’t recognize "C2 CompilerThread*" samples. This makes the test brittle across JVM configurations (tiered vs non-tiered).
    ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:63
  • On HotSpot this test hardcodes a C1-only thread-name prefix (and it’s truncated), but compiler threads can also be named "C2 CompilerThread*". That can make the test fail even when TLS priming works (or pass/fail depending on tiered compilation).

This issue also appears on line 82 of the same file.
ddprof-lib/src/main/cpp/threadLocalDataPool.h:13

  • This header forward-declares ProfiledThread, but the inline contains() implementation does pointer arithmetic on ProfiledThread* ("_threads + _capacity"), which requires ProfiledThread to be a complete type at include time. That creates a fragile include-order dependency (and is easy to break in future TUs).
#include <stdint.h>
#include <stdlib.h>
#include <new>

class ProfiledThread;

ddprof-lib/src/main/cpp/threadLocalDataPool.h:79

  • destroyForTest() manually replicates destructor logic and claims there is no destructor definition, but under UNIT_TEST a destructor is declared/defined. Using delete here keeps test cleanup aligned with future destructor changes and avoids misleading comments.
    // ThreadLocalDataPool has no destructor definition (it's a process-lifetime
    // singleton in production, never freed), so `delete p` won't link. Mirror
    // what a destructor would do -- destroy each placement-newed ProfiledThread
    // and free() the malloc'd buffer -- then release the ThreadLocalDataPool
    // object itself via the deallocation function directly, without invoking a
    // (nonexistent) destructor.

ddprof-lib/src/main/cpp/unwindStats.cpp:22

  • UnwindFailures::reset() zeroes the full MAX_UNWIND_FAILURE_NAMES buffers (~300KB) every time it’s called. Because reset() is invoked from ProfiledThread::resetClaimed() during TLS priming, this can add a large, avoidable memset cost on the signal path. Clearing only the entries that were actually used (based on _nameCount) keeps semantics while reducing worst-case work.
void UnwindFailures::reset() {
    memset((void*)_names, 0, MAX_UNWIND_FAILURE_NAMES * MAX_NAME_LENGTH);
    memset((void*)_counters, 0, MAX_UNWIND_FAILURE_NAMES * (UNWIND_FAILURE_ANY + 1) * sizeof(u64));
    _nameCount = 0;
}

ddprof-lib/src/main/cpp/guards.h:47

  • This comment reads ungrammatically ("null or via thread priming on a thread — …") and is hard to parse. Clarifying the wording will make the guard’s behavior around primed (pool-backed) threads easier to understand.
// When ProfiledThread is null or via thread priming on a thread
// — uninstrumented JVM-internal threads (VM Thread, JIT, GC) fall
// into this bucket too, and they can receive signals.  The

Comment thread ddprof-lib/src/main/cpp/threadLocalData.inline.h Outdated
@dd-octo-sts

dd-octo-sts Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 13c6c30)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/129278347 Commit: 13c6c30d0571b6155b7c99421110708c4b6bfc4a

⚠️ Significant outliers

  • 🔴 future-genetic (JDK 21): runtime +5.7% (2046→2162 ms)
  • 🔴 future-genetic (JDK 25): runtime +4.6% (1927→2016 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10172 ms (21 iters) ✅ 10171 ms (21 iters) ≈ -0% (±9.9%) — / —
akka-uct 25 ✅ 8885 ms (24 iters) ✅ 8782 ms (24 iters) ≈ -1.2% (±9.5%) — / —
finagle-chirper 21 ✅ 6028 ms (33 iters) ✅ 5938 ms (33 iters) ≈ -1.5% (±25.4%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5366 ms (36 iters) ✅ 5422 ms (36 iters) ≈ +1% (±23.1%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2818 ms (66 iters) ✅ 2824 ms (66 iters) ≈ +0.2% (±2.6%) — / —
fj-kmeans 25 ✅ 2749 ms (68 iters) ✅ 2816 ms (66 iters) ≈ +2.4% (±2.6%) — / —
future-genetic 21 ✅ 2046 ms (91 iters) ✅ 2162 ms (86 iters) 🔴 +5.7% — / —
future-genetic 25 ✅ 1927 ms (95 iters) ✅ 2016 ms (92 iters) 🔴 +4.6% — / —
naive-bayes 21 ✅ 1279 ms (134 iters) ✅ 1290 ms (133 iters) ≈ +0.9% (±32.1%) — / —
naive-bayes 25 ✅ 987 ms (173 iters) ✅ 1016 ms (168 iters) ≈ +2.9% (±32.3%) — / —
reactors 21 ✅ 15571 ms (16 iters) ✅ 15913 ms (15 iters) ≈ +2.2% (±7.8%) — / —
reactors 25 ✅ 18159 ms (15 iters) ✅ 18763 ms (15 iters) ≈ +3.3% (±4.4%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 1 / 1 1988 / 1943 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 4 / 4 2382 / 2326 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 3 / 3 8465 / 8792 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 1 / 1 7984 / 8296 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ 1 / 2 1253 / 1280 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 2 / 1 1272 / 1262 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 2 / ✅ 2990 / 3013 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 1 / 2 2871 / 2942 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 6 / 1 3531 / 3537 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 2 / 2 3448 / 3461 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ 1 / 1 1622 / 1665 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1836 / 1932 ✅ / ✅ ✅ / ✅

@zhengyu123
zhengyu123 marked this pull request as draft August 5, 2026 22:03
Copilot AI review requested due to automatic review settings August 5, 2026 23:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.

Suppressed comments (5)

ddprof-lib/src/main/cpp/threadLocalData.cpp:125

  • resetClaimed() reuses a ProfiledThread slot for a different OS thread, but it doesn’t reset per-thread state inherited from ThreadLocalData/ProfiledThread (notably _unwinding_Java and _cpu_epoch). If a recycled slot previously had _unwinding_Java=true, AsyncSampleMutex can be permanently disabled for the new thread; leaving _cpu_epoch non-zero also makes per-thread epoch tracking depend on prior occupants.
void ProfiledThread::resetClaimed(int tid) {
  _jmp_buf = nullptr;
  _pc = 0;
  _sp = 0;
  _span_id = 0;

ddprof-lib/src/main/cpp/threadLocalDataPool.h:79

  • The UNIT_TEST helpers have two concrete issues: (1) createForTest() takes a uint64_t but the constructor takes uint16_t, so the capacity is silently narrowed/truncated; (2) destroyForTest()’s comment and implementation claim the destructor is unavailable, but ~ThreadLocalDataPool is declared/defined under UNIT_TEST, so this duplicated manual cleanup is unnecessary and can drift from the real destructor behavior.
    static ThreadLocalDataPool* createForTest(uint64_t capacity) {
        return new ThreadLocalDataPool(capacity);
    }
    // ThreadLocalDataPool has no destructor definition (it's a process-lifetime
    // singleton in production, never freed), so `delete p` won't link. Mirror

ddprof-lib/src/main/cpp/threadLocalDataPool.cpp:38

  • In UNIT_TEST builds the constructor records this pool’s allocation in NativeMem (NM_THREAD_LOCAL), but the destructor never decrements it. When tests create/destroy pools (e.g. threadLocalDataPool_ut.cpp), this makes NM_THREAD_LOCAL accounting monotonically grow within the test process.
ThreadLocalDataPool::~ThreadLocalDataPool() {
    if (_threads != nullptr) {
        for (int index = 0; index < _capacity; index++) {
            _threads[index].~ProfiledThread();
        }

ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:104

  • On HotSpot, compiler thread names are typically "C1 CompilerThread*" and/or "C2 CompilerThread*". The current check only looks for one prefix, so the test can falsely fail on configurations that only run C2 compiler threads (or report a misleading failure message).
    ddprof-lib/src/main/cpp/mallocTracer.cpp:46
  • Comment grammar/wording: this code is executed inside malloc hooks, so the concern is infinite recursion/re-entrancy, not an "indefinite loop". Tweaking the wording will make the rationale clearer.
        // Even we are not in a signal handler, we cannot malloc or
        // we may get into indefinite loop
        ProfiledThread::acquireCurrent();

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 45 changed files in this pull request and generated 1 comment.

Suppressed comments (9)

ddprof-lib/src/test/cpp/stress_callTraceStorage.cpp:1125

  • This signal handler uses SIGNAL_HANDLER_GUARD_NO_SAMPLE(), but then unconditionally enters a CriticalSection. If SignalHandlerScope could not attach a ProfiledThread (e.g., pool exhausted / priming disabled), CriticalSection will assert/crash. Add an explicit isActive() check (or use SIGNAL_HANDLER_GUARD()) before proceeding.
    ddprof-lib/src/main/cpp/stackWalker.cpp:137
  • Typo in assert message: "entery" -> "entry".
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/threadLocalDataPool.h:79

  • The UNIT_TEST helper comment/implementation for destroyForTest is inconsistent with this file: a UNIT_TEST destructor is declared for ThreadLocalDataPool, so delete p does link and is simpler than manually destructing elements and calling ::operator delete(p). Keeping the manual teardown risks drifting from the actual destructor behavior over time.
    // ThreadLocalDataPool has no destructor definition (it's a process-lifetime
    // singleton in production, never freed), so `delete p` won't link. Mirror
    // what a destructor would do -- destroy each placement-newed ProfiledThread
    // and free() the malloc'd buffer -- then release the ThreadLocalDataPool
    // object itself via the deallocation function directly, without invoking a

ddprof-lib/src/test/cpp/stress_callTraceStorage.cpp:1094

  • This signal handler uses SIGNAL_HANDLER_GUARD_NO_SAMPLE(), but then unconditionally enters a CriticalSection. If SignalHandlerScope could not attach a ProfiledThread (e.g., pool exhausted / priming disabled), CriticalSection will assert/crash. Add an explicit isActive() check (or use SIGNAL_HANDLER_GUARD()) before proceeding.

This issue also appears on line 1121 of the same file.
ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:56

  • HotSpot compiler thread names are typically "C1 CompilerThread*" and/or "C2 CompilerThread*". The current prefix string is missing the trailing characters ("CompilerThre"), which makes the intent unclear and reads like a typo.
    ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:101
  • On HotSpot, this test only matches threads starting with the C1 compiler prefix, but sampling can land exclusively on C2 compiler threads on some runs/JVM configurations. Consider accepting both C1 and C2 compiler thread prefixes so the test doesn't falsely fail when priming works but only C2 is observed.
    ddprof-lib/src/main/cpp/stackWalker.cpp:50
  • Typo in assert message: "entery" -> "entry".

This issue also appears on line 136 of the same file.

    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/wallClock.cpp:462

  • Typo in assert message: "entery" -> "entry".
    ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp:246
  • Typo in assert message: "entery" -> "entry".
    ProfiledThread* prof_thread = ProfiledThread::current();
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

Comment thread ddprof-lib/src/main/cpp/javaApi.cpp
@dd-octo-sts

dd-octo-sts Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

All 40 integration tests passed

📊 Dashboard · 👷 Pipeline · 📦 1eede8ff

zhengyu123 and others added 2 commits August 7, 2026 16:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 45 changed files in this pull request and generated no new comments.

Suppressed comments (6)

ddprof-lib/src/main/cpp/stackWalker.cpp:137

  • Typo in assert message: "entery" should be "entry".
    ProfiledThread* prof_thread = ProfiledThread::current();
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/wallClock.cpp:461

  • Typo in assert message: "entery" should be "entry".
    ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:101
  • On HotSpot, compiler thread names can start with "C2 CompilerThread" (not just "C1 CompilerThread"). Using a single expectedPrefix means the test can miss valid compiler-thread samples and fail spuriously; also the current constant value ("C1 CompilerThre") looks truncated/typoed. Match both C1 and C2 compiler thread prefixes (or otherwise detect HotSpot compiler threads) when counting samples and building the failure message.
    ddprof-lib/src/main/cpp/stackWalker.cpp:49
  • Typo in assert message: "entery" should be "entry".

This issue also appears on line 135 of the same file.

    ProfiledThread* prof_thread = ProfiledThread::current();
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp:245

  • Typo in assert message: "entery" should be "entry".
    ProfiledThread* prof_thread = ProfiledThread::current();
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/threadLocalDataPool.h:79

  • The UNIT_TEST build does define ThreadLocalDataPool::~ThreadLocalDataPool() in threadLocalDataPool.cpp, so the comment claiming "delete p won't link" / "nonexistent destructor" is inaccurate. This also duplicates the destructor logic in two places; using delete here keeps cleanup behavior centralized.
    // ThreadLocalDataPool has no destructor definition (it's a process-lifetime
    // singleton in production, never freed), so `delete p` won't link. Mirror
    // what a destructor would do -- destroy each placement-newed ProfiledThread
    // and free() the malloc'd buffer -- then release the ThreadLocalDataPool
    // object itself via the deallocation function directly, without invoking a
    // (nonexistent) destructor.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 45 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

ddprof-lib/src/main/cpp/stackWalker.cpp:134

  • Typo in the assert message: "entery" -> "entry" (and "setup" -> "set up").
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/wallClock.cpp:460

  • Typo in the assert message: "entery" -> "entry" (and "setup" -> "set up").
    ddprof-test/src/test/java/com/datadoghq/profiler/cpu/TlsPrimingTest.java:96
  • On HotSpot, compiler threads can be named both "C1 CompilerThread*" and "C2 CompilerThread*". The current startsWith(expectedPrefix) check only matches the C1 prefix, so the test can miss valid compiler-thread samples and fail depending on the JVM configuration. Match both C1 and C2 explicitly.
    ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp:245
  • Typo in the assert message: "entery" -> "entry" (and "setup" -> "set up").
    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/stackWalker.cpp:49

  • Typo in the assert message: "entery" -> "entry" (and "setup" -> "set up").

This issue also appears on line 134 of the same file.

    assert(prof_thread != nullptr && "Should have been setup at signal handler entery");

ddprof-lib/src/main/cpp/threadLocalDataPool.h:80

  • destroyForTest() frees the pool and its backing buffer but doesn't undo the NativeMem::record(NM_THREAD_LOCAL, ...) done in the constructor. This makes NM_THREAD_LOCAL accounting drift in unit tests (and can mask leaks or break tests that assert exact deltas). Record the corresponding negative amount before deallocating the ThreadLocalDataPool object.
    static void destroyForTest(ThreadLocalDataPool* p) {
        if (p->_threads != nullptr) {
            for (uint64_t index = 0; index < p->_capacity; index++) {
                p->_threads[index].~ProfiledThread();
            }
            free(reinterpret_cast<void*>(p->_threads));
        }
        ::operator delete(p);
    }

ddprof-lib/src/main/cpp/guards.cpp:58

  • SignalHandlerScope::~SignalHandlerScope() correctly uses the captured _current pointer, but SignalHandlerScope::release() still re-reads TLS via ProfiledThread::current(). In the clearCurrentThreadTLS race window, TLS can be nullptr while the captured pointer is still valid; since SIGNAL_HANDLER_GUARD_RELEASE() is used specifically to avoid leaking signal-depth when chaining to a handler that may siglongjmp, release() should also use _current (like the destructor) instead of relying on TLS.
SignalHandlerScope::~SignalHandlerScope() {
    if (!_active) return;
    if (_current != nullptr) {
        _current->exitSignalScope();
    }

Comment thread ddprof-lib/src/main/cpp/profiler.cpp
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sphinx:spotcheck Sphinx: spot-check recommended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants